---
title: Hearing scheduling and calendar sync
summary: Create/update/delete hearing schedules and sync Exchange calendars.
status: draft
owner: SAIF Documentation Generator
last_reviewed: 2026-08-26
library: SAIF.Business.Claim.Legal
reference_type: embedded
operation_id: hearing-scheduling-and-calendar-sync
trigger: A save or delete is performed on clsLegalSchedule
---

# Hearing scheduling and calendar sync

Persists a legal-case hearing schedule and keeps the assigned attorney/ALJ Outlook/Exchange calendars
in sync with the schedule's lifecycle.

| Field | Value |
| --- | --- |
| **Operation ID** | hearing-scheduling-and-calendar-sync |
| **Trigger** | Save or delete on `clsLegalSchedule` |
| **Inputs** | Schedule fields (type, status, date/time, attorney, ALJ, hearing location, duration) |
| **Outputs** | Persisted schedule row, Outlook/Exchange calendar appointments |
| **Primary components** | `clsLegalSchedule.BeforeInsert`/`AfterInsert`/`BeforeUpdate`/`AfterUpdate`/`BeforeDelete`/`AfterDelete`, `clsLegalSchedule.UpdateCalendars`, `clsLegalSchedule.RemoveFromCalendars` |
| **Entry point** | `SAIF.Business.Claim.Legal\LegalNet\clsLegalSchedule.vb` |

## Steps

1. Rule validation runs on `Save()`, checking these real conditions on the object's fields:
   - `Rule_ValidTypeCode`/`Rule_ValidStatusCode` — `ScheduleTypeCode.Trim.Length > 0` and
     `ScheduleStatusCode.Trim.Length > 0`; either field left blank blocks the save
     ([BR-SC-001](../business-rules.md#br-sc-001-schedule-type-and-status-are-required)).
   - `Rule_ValidDate` — only runs when `IsSetorReset` (status is `Set` or `Reset`) and the object is
     dirty; requires `ScheduleDate > Today`
     ([BR-SC-002](../business-rules.md#br-sc-002-schedule-date-must-be-in-the-future-for-schedules-in-set)).
   - `Rule_ValidAttorney` — fails when both `IsAssignedToOutsideCounsel` (an other-firm ID is set) and
     `IsAssignedToSaifAttorney` (a non-blank, non-"0" attorney employee ID is set) are true at the same
     time ([BR-SC-003](../business-rules.md#br-sc-003-a-schedule-may-not-have-both-a-saif-attorney-and-outside)).
   - `Rule_ValidOutsideCounsel` — fails when `AssignedOtherFirmID > 0` but `AssignedPersonID = 0`, or
     when `AssignedOtherFirmID = 0` but `AssignedPersonID > 0`; a firm and an attorney-at-the-firm must
     both be set or both be blank
     ([BR-SC-004](../business-rules.md#br-sc-004-outside-counsel-requires-both-a-firm-and-an-attorney)).
   - `Rule_ValidScheduleDate`/`Rule_ValidTime` — require `ScheduleDate > Date.MinValue` and
     `ScheduleTime.TimeOfDay.Hours > 0`
     ([BR-SC-005](../business-rules.md#br-sc-005-schedule-date-and-time-are-required)).
   - `Rule_ValidTimeRange` (LevelTwo) — requires the scheduled hour to be between 7 and 18 inclusive
     ([BR-SC-006](../business-rules.md#br-sc-006-schedule-time-must-be-between-700-am-and-600-pm)).
   - `Rule_ValidTimeDuration` (LevelTwo) — when `DurationCode = "AM"`, the hour must be in `[7, 12)`;
     when `DurationCode = "PM"`, the hour must be in `[12, 18]`; any other duration code passes without
     a time-range check
     ([BR-SC-007](../business-rules.md#br-sc-007-duration-must-be-consistent-with-the-scheduled-time)).
   - `Rule_ValidDuration` — requires `DurationCode.Trim.Length > 0`
     ([BR-SC-008](../business-rules.md#br-sc-008-duration-is-required)).
2. On insert: `Create()` (invoked before `BeforeInsert`) sets `LegalCaseID` from the criteria,
   assigns a new `ScheduleID` from the `SCHEDULE_SEQ` sequence, stamps `CreateDate = Now` and
   `CreateUser` from the portal user, and defaults `ScheduleStatusCode = "SET"`. `BeforeInsert` then
   builds the insert parameter list from every schedule field (type, status, keep-attorney flag,
   date/time, ALJ, hearing location, attorney/other-firm/person IDs, duration, assigned/reminder dates,
   schedule text), clearing the attorney employee ID to empty when it is `"0"` and clearing the
   other-firm/person IDs to empty when they are `0` so blank associations aren't stored as literal
   zeros. The framework issues the insert stored procedure, and `AfterInsert` checks the
   `RowsAffected` return parameter: if it is not exactly `1` it adds an `InsertError`, otherwise it
   marks the object no longer new. Calendar sync is not triggered directly from `AfterInsert` in this
   class — `UpdateCalendars()` is available as a public method that callers (such as the assignment
   process) invoke to push schedule data into Outlook/Exchange.
3. On update: `BeforeUpdate` builds the same field list as `BeforeInsert` (minus create-only fields,
   plus `UpdateUser`/`UpdateDate`, which `OnSave()` stamps to `Now`/the current portal user just
   before the base save runs) for the update stored procedure. `AfterUpdate` performs the identical
   `RowsAffected <> 1` check, adding an `InsertError` message (the same error key/text as the insert
   path is reused here) on failure or marking the object clean on success. `UpdateCalendars()` calls
   `clsAssignmentProcess.updateOutlook("CASE", LegalCaseID, UserID)`, which re-reads the case's
   `clsOutlookFields` rows and, for each one, rebuilds a `saifAppt` keyed by `"S" + ScheduleID` (zero
   padded to 8 digits): the subject is composed from schedule-type initial, status code (when not
   `HearingSet`), claim number, claimant name, schedule time, and either the ALJ name or the free-text
   schedule note, with `" - WT 3"` appended for weight-3 cases and `" - CONSOL"` appended for
   consolidated cases; the appointment's mailbox (`UserName`) is the assigned attorney's mail ID; start
   and end times are derived from `DurationCode` (`AM` → 8:00 AM–12:00 PM, `PM` → 1:00 PM–5:00 PM,
   `FULL` → 8:00 AM–5:00 PM same day, any multi-day duration code → 8:00 AM start through 5:00 PM on
   `ScheduleDate + (DaysNum - 1)` days); if the schedule date is in the future and the status is
   `Postponed`, `Cancelled`, or `Dismissed` the appointment is deleted instead of upserted. The actual
   Exchange call (`clsAssignmentProcess.callSaifExUpdate`) then creates or updates the appointment via
   `clsExchangeUpdate.UpdateAppointment` — there's no separate lookup step to decide create-vs-update;
   the Exchange service resolves that by the appointment's `ExID`. In non-production environments the
   mailbox ID is remapped to one of several fixed test mailboxes (`ztstatt2`/`ztstatt3`/`ztstatt4`)
   based on the first letter of the real mail ID, so calendar writes never reach a real attorney's
   mailbox outside production
   ([BR-SC-009](../business-rules.md#br-sc-009-a-case-may-have-only-one-schedule-in-set-or-reset-status)).
4. On delete: `BeforeDelete` builds the delete stored-procedure call keyed by `ScheduleID` and writes
   an audit-log entry ("Deleting Schedule Record From Oracle") recording the schedule ID and user ID;
   it performs no additional business validation beyond that. `AfterDelete` checks `RowsAffected <> 1`
   (adds a `DeleteError` on failure) and otherwise marks the object new and calls
   `RemoveFromCalendars()`. `RemoveFromCalendars()` only acts if the object is dirty and the attorney
   employee ID's *original* (pre-change) value is non-blank: it resolves that original attorney's mail
   ID, builds an appointment ID of `"S" + ScheduleID`, and calls `callSaifExUpdate` with the delete flag
   set, searching an Exchange window from the original schedule date through 7 days later. For
   multi-day durations (`DurationCode` ending in `"DAY"`), it repeats the delete once per day, appending
   a zero-padded day suffix (`-00`, `-01`, and so on) to each appointment ID so every day's calendar
   entry is individually removed.
5. `GetLegalSecretaryID(inAssignedAttorneyID)` runs
   `SELECT secretary_emplid FROM attorney_profile WHERE profile_emplid = :ID` for the given attorney ID
   (or the `OUTSIDECNCL` constant when the schedule is assigned to outside counsel), returning an empty
   string if no row is found; it backs the `LegalSecretary` property, not the calendar appointment
   itself. `GetALJName()` runs
   `SELECT last_name || ', ' || first_name FROM legal_person WHERE legal_person_seq_num = <ALJPersonID>`
   and returns an empty string if no match is found; its result is what `updateOutlook` uses to append
   `" - <ALJName>"` to the appointment subject when an ALJ is assigned.

## Process flow

```mermaid
flowchart TD
	Start(["Save or delete<br>clsLegalSchedule"]) --> Rules{"Rules valid?"}
	Rules -->|No| Err(["Save blocked,<br>errors surfaced"])
	Rules -->|Yes| Op{"Operation?"}
	Op -->|Insert| BI["BeforeInsert:<br>build insert params,<br>blank zero IDs"] --> Ins["Framework insert"] --> AI["AfterInsert:<br>check RowsAffected,<br>MarkNew"]
	Op -->|Update| BU["BeforeUpdate:<br>build update params"] --> Upd["Framework update"] --> AU["AfterUpdate:<br>check RowsAffected"] --> UC["UpdateCalendars:<br>rebuild Outlook appt<br>via updateOutlook"]
	Op -->|Delete| BD["BeforeDelete:<br>audit log entry"] --> Del["Framework delete"] --> AD["AfterDelete:<br>check RowsAffected,<br>MarkNew"] --> RC["RemoveFromCalendars:<br>delete Exchange appt(s)"]
	AI --> EndOk(["Save complete"])
	UC --> EndOk
	RC --> EndOk
```

## Failure and edge-case behavior
- `AfterInsert`/`AfterUpdate`/`AfterDelete` check `RowsAffected <> 1` and raise an error message
  (`InsertError` on insert/update, `DeleteError` on delete) when the check fails, consistent with the
  library's optimistic-concurrency pattern.
- `RemoveFromCalendars()` only removes an appointment when the object is dirty and the attorney
  employee ID had a non-blank original value; a delete with no prior attorney assignment leaves no
  Exchange appointment to remove and is a no-op.
- Non-production environments redirect calendar mailbox operations to one of several fixed test
  mailboxes (`ztstatt2`/`ztstatt3`/`ztstatt4`, selected by the first letter of the real mail ID) rather
  than the real attorney/ALJ mailbox.
- `callSaifExUpdate` and `findAppointment` swallow Exchange-service exceptions with `clsLogError` when
  running in a web request context, but re-throw them when running in batch mode (no
  `HttpContext.Current`).*

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [Database tables](../database-tables.md)
- [SQL catalog](../sql/index.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Sources
- `SAIF.Business.Claim.Legal\LegalNet\clsLegalSchedule.vb`
- `SAIF.Business.Claim.Legal\LegalNet\clsAssignmentProcess.vb`
